package com.dana1.nos.old.impl.memory;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.collections.keyvalue.MultiKey;
import org.apache.commons.lang.Validate;

import com.dana1.nos.old.ObjectKey;
import com.dana1.nos.old.ServiceObject;
import com.dana1.nos.old.impl.AbstractObjectService;
import com.dana1.nos.old.meta.InverseOf;
import com.dana1.nos.old.util.PropertyUtils;

public class MemoryObjectService extends AbstractObjectService
{
	private Map<Class<? extends ServiceObject>,Map<ObjectKey<? extends ServiceObject>,ServiceObject>> objectsByClassAndKey = new HashMap<Class<? extends ServiceObject>, Map<ObjectKey<? extends ServiceObject>,ServiceObject>>();
	private Map<String,Map<ObjectKey<?>,List<ObjectKey<?>>>> mmRelationsByNameAndOwnerKey = new HashMap<String, Map<ObjectKey<?>,List<ObjectKey<?>>>>();
	private Map<MultiKey,Set<ObjectKey<?>>> m1Lists = new HashMap<MultiKey, Set<ObjectKey<?>>>();
	
	// search operations
	@Override public <T extends ServiceObject> List<ObjectKey<T>> query(T obj) { return null; }

	// passthru operation. The multiple sql statements are so that a true list of affected object keys can be returned by the underlying driver
	@Override public List<ObjectKey<?>> execute(Map<String,Object> parameters, String ... sql) { return null; }

	
	// Object operations
	@Override 
	public <T extends ServiceObject> ObjectKey<T> insert(T obj) 
	{
		ObjectKey<T> key=objectKey(obj);
		addObjectByKey(key,obj);
		return key;
	};
	@Override
	public <T extends ServiceObject> T retrieve(ObjectKey<T> key)
	{
		T obj = getObjectByKey(key);
		if (obj!=null)
		{
			handlerFor(obj).setConnected(true);
		}
		return obj;
	}
	@Override
	public <T extends ServiceObject> void update(T obj) {
		// noop
	};
	@Override
	public <T extends ServiceObject> void delete(ObjectKey<T> key)
	{
		T obj = removeObjectByKey(key);
		if (obj!=null)
			handlerFor(obj).setConnected(false);
	}
	// relation operations
	@Override 
	public List<ObjectKey<?>> retrieveMM(ObjectKey<? extends ServiceObject> ownerKey,String relationName) 
	{ 
		synchronized(mmRelationMapFor(relationName))
		{
			return (List<ObjectKey<?>>) new ArrayList<ObjectKey<?>>(mmRelationFor(relationName, ownerKey));
		}
	}
	@Override 
	public int countMM(ObjectKey<? extends ServiceObject> ownerKey,String relationName) 
	{ 
		synchronized(mmRelationMapFor(relationName))
		{
			return mmRelationFor(relationName, ownerKey).size(); 
		}
	}
	@Override 
	public boolean addMM(ObjectKey<? extends ServiceObject> ownerKey, String relationName, ObjectKey<? extends ServiceObject> childKey) 
	{
		ServiceObject obj = retrieve(childKey);
		if ((null==obj)||(!handlerFor(obj).isConnected()))
			throw new UnsupportedOperationException("The child element must be connected before insertion");

		synchronized(mmRelationMapFor(relationName))
		{
			mmRelationFor(relationName, childKey).add(ownerKey);
			return mmRelationFor(relationName, ownerKey).add(childKey);
		}
	}
	@Override 
	public boolean removeMM(ObjectKey<? extends ServiceObject> ownerKey, String relationName, ObjectKey<? extends ServiceObject> childKey) 
	{
		synchronized(mmRelationMapFor(relationName))
		{
			mmRelationFor(relationName, childKey).remove(ownerKey);
			return mmRelationFor(relationName, ownerKey).remove(childKey);
		}
	}
	
	// relation operations (inverse FK)
	@Override 
	public <T extends ServiceObject> List<ObjectKey<T>> retrieve1M(ObjectKey<?> childKey,Class<T> parentObjectClass,String relationName) 
	{ 
		if (Proxy.isProxyClass(parentObjectClass))
			throw new IllegalArgumentException("parentObjectClass must not be a proxy class");
		return new ArrayList<ObjectKey<T>>(get1MList(childKey,relationName,parentObjectClass));
	}
	@Override 
	public int count1M(ObjectKey<? extends ServiceObject> childKey,Class<? extends ServiceObject> parentObjectClass,String relationName) 
	{ 
		return retrieve1M(childKey, parentObjectClass, relationName).size(); 
	}
	@SuppressWarnings("unchecked")
	@Override 
	public boolean add1M(ObjectKey<? extends ServiceObject> childKey, String relationName, ObjectKey<? extends ServiceObject> parentKey) 
	{ 
		ObjectKey oldChildKey = (ObjectKey)handlerFor(retrieveOrCheat(parentKey)).getPropertyValues().put(relationName, childKey);
		if (null!=oldChildKey)
			get1MList(oldChildKey,relationName,parentKey.getServiceObjectClass()).remove(parentKey);
		if (null!=childKey)
			get1MList(childKey,relationName,parentKey.getServiceObjectClass()).add((ObjectKey) parentKey);
		return true; 
	}
	@Override public boolean remove1M(ObjectKey<? extends ServiceObject> childKey, String relationName, ObjectKey<? extends ServiceObject> parentKey) 
	{ 
		handlerFor(retrieveOrCheat(parentKey)).getPropertyValues().remove(relationName);
		get1MList(childKey,relationName,parentKey.getServiceObjectClass()).remove(parentKey);
		return true; 
	}

	@Override public boolean set11(ObjectKey<? extends ServiceObject> ownerKey, String relationName, ObjectKey<? extends ServiceObject> childKey) 
	{ 
		PropertyDescriptor propDesc = PropertyUtils.getPropertyDescriptor(ownerKey.getServiceObjectClass(), Object.class,relationName);
		Method m = PropertyUtils.getInterfaceMethod(ownerKey.getServiceObjectClass(), propDesc);
		String inverseProperty = m.getAnnotation(InverseOf.class).value();
		
		ObjectKey<?> old = (ObjectKey<?>)handlerFor(retrieve(ownerKey)).getPropertyValues().put(relationName,childKey);

		if ((null!=old)&&(old!=childKey)) 
			handlerFor(retrieve(old)).getPropertyValues().remove(inverseProperty);

		if (null!=childKey)
			old = (ObjectKey<?>)handlerFor(retrieve(childKey)).getPropertyValues().put(inverseProperty, ownerKey);
		else
			old = null;
		
		if ((null!=old)&&(old!=ownerKey))
			handlerFor(retrieve(old)).getPropertyValues().remove(relationName);
	
		return true;
	}

	@SuppressWarnings("unchecked")
	private <T extends ServiceObject> Set<ObjectKey<T>> get1MList(ObjectKey<? extends ServiceObject> childKey, String relationName, Class<T> serviceObjectClass)
	{
		Set result;
		synchronized(m1Lists)
		{
			if (null==(result=m1Lists.get(new MultiKey(serviceObjectClass,relationName,childKey))))
				m1Lists.put(new MultiKey(serviceObjectClass,relationName,childKey), result = new TreeSet<ObjectKey<?>>());
		}
		return result;
	}

	private List<ObjectKey<?>> mmRelationFor(String relationName, ObjectKey<? extends ServiceObject> ownerKey)
	{
		Map<ObjectKey<?>,List<ObjectKey<?>>> resultMap = mmRelationMapFor(relationName);
		List<ObjectKey<?>> result;
		synchronized(resultMap)
		{
			if (null==(result = resultMap.get(ownerKey)))
				resultMap.put(ownerKey,result = new ArrayList<ObjectKey<?>>());
		}
		return result;
	}

	private Map<ObjectKey<?>,List<ObjectKey<?>>> mmRelationMapFor(String relationName)
	{
		Map<ObjectKey<?>,List<ObjectKey<?>>> result = null;
		synchronized(mmRelationsByNameAndOwnerKey)
		{
			if (null==(result=mmRelationsByNameAndOwnerKey.get(relationName)))
				mmRelationsByNameAndOwnerKey.put(relationName, result = Collections.synchronizedMap(new HashMap<ObjectKey<?>, List<ObjectKey<?>>>()));
		}
		return result;
	}

	private <T extends ServiceObject> T addObjectByKey(ObjectKey<T> key, T obj)
	{
		Map<ObjectKey<?>, ServiceObject> objectsByKey = getObjectsByKey(key.getServiceObjectClass());
		synchronized(objectsByKey)
		{
			objectsByKey.put(key,obj);
		}
		return obj;
	}
	@SuppressWarnings("unchecked")
	private <T extends ServiceObject> T getObjectByKey(ObjectKey<T> key)
	{
		if (null==key)
			return null;
		Map<ObjectKey<?>, ServiceObject> objectsByKey = getObjectsByKey(key.getServiceObjectClass());
		synchronized(objectsByKey)
		{
			return (T)objectsByKey.get(key);
		}
	}
	@SuppressWarnings("unchecked")
	private <T extends ServiceObject> T removeObjectByKey(ObjectKey<T> key)
	{
		Map<ObjectKey<?>,ServiceObject> objectsByKey = getObjectsByKey(key.getServiceObjectClass()); 
		synchronized(objectsByKey)
		{
			return (T)objectsByKey.remove(key);
		}
	}

	private Map<ObjectKey<?>, ServiceObject> getObjectsByKey(Class<? extends ServiceObject> objClass)
	{
		Validate.isTrue(!Proxy.isProxyClass(objClass),"Cannot specify a proxy class to this function");
		Map<ObjectKey<?>,ServiceObject> objectsByKey; 
		synchronized(objectsByClassAndKey)
		{
			if (null==(objectsByKey = objectsByClassAndKey.get(objClass)))
				objectsByClassAndKey.put(objClass,objectsByKey = new HashMap<ObjectKey<?>,ServiceObject>());
		}
		return objectsByKey;
	}

}
